// FacebookApplication.cs
// Facebook/FrameworkWeb/Web
// Copyright (c) 2007, Nikhil Kothari. All Rights Reserved.
//

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Web;
using System.Web.UI;
using Facebook.Service;
using Facebook.Service.Core;
using Facebook.Web.Configuration;

// TODO: Update DefaultProperty to be ApplicationName
//       Add designer support for ApplicationName property

namespace Facebook.Web
{

	/// <summary>
	/// Represents a Facebook application. This control manages establishing a session
	/// and provides access to the Facebook APIs.
	/// Only one instance of this control can be placed on the page.
	/// </summary>
	[
	ControlValueProperty("UserID"),
	DefaultEvent("SessionCreated"),
	DefaultProperty("ApplicationKey"),
	Designer(typeof(Facebook.Design.Web.FacebookApplicationDesigner)),
	NonVisualControl
	]
	public class FacebookApplication : Control
	{

		private static readonly object SessionCreatingEvent = new object();
		private static readonly object SessionCreatedEvent = new object();

		private FacebookApplicationMode _mode;
		private FacebookSession _session;
		private FacebookService _service;
		private bool _enableDebugging;
		private bool _enableExternalBrowsing;
		private bool _enableAjaxScenarios;

		private string _applicationName;
		private string _applicationKey;
		private string _secret;

		private object _controlState;

		private bool _isAppAdded;
		private bool _inCanvasPage;
		private bool _inIFramePage;
		private bool _inNewProfile;

		private bool _requiresAppAdded = true;
		private bool _requiresLogin = true;
		private bool _redirectCanvas = false;
		private string _redirectNext;

		/// <summary>
		/// The application key used to uniquely identify the application that is available
		/// at application registration time.
		/// This can only be set during initialization of the page.
		/// </summary>
		[
		Category("Behavior"),
		DefaultValue(""),
		Description("The application key that is used to identify a Facebook application.")
		]
		public string ApplicationKey
		{
			get
			{
				if (_applicationKey == null)
				{
					return String.Empty;
				}
				return _applicationKey;
			}
			set
			{
				if (_session != null)
				{
					throw new InvalidOperationException("ApplicationKey can only be set during page initialization.");
				}
				_applicationKey = value;
			}
		}

		/// <summary>
		/// The name of the application. This is used to create the application's canvas
		/// page URL, i.e. http://apps.facebook.com/[name]/.
		/// On the server, this is used to load settings from configuration. Specifically,
		/// the facebook section in configuration is looked up for an application object
		/// with the given name.
		/// </summary>
		[
		Category("Behavior"),
		DefaultValue(""),
		Description("The application name that is used to load Application Key and Secret from configuration.")
		]
		public string ApplicationName
		{
			get
			{
				if (_applicationName == null)
				{
					return String.Empty;
				}
				return _applicationName;
			}
			set
			{
				if (_session != null)
				{
					throw new InvalidOperationException("ApplicationName can only be set during page initialization.");
				}
				_applicationName = value;
			}
		}

		/// <summary>
		/// Whether the application must be added in order to access the page on which 
		/// this control resides.
		/// </summary>
		[
		Category("Behavior"),
		DefaultValue(true),
		Description("Whether the application must be added in order to view the page.")
		]
		public bool RequiresAppAdded
		{
			get
			{
				return _requiresAppAdded;
			}
			set
			{
				_requiresAppAdded = value;
			}
		}

		/// <summary>
		/// Whether the user must log in to the application in order to access the page on which 
		/// this control resides.
		/// If this is set to false and the user has not added or previously logged in to the 
		/// application (maybe to access a different page,) then the userId and session key will 
		/// not be available. These should not be assumed to exist on a page that does not 
		/// require login.
		/// </summary>
		[
		Category("Behavior"),
		DefaultValue(true),
		Description("Whether the user must log in to view the page.")
		]
		public bool RequiresLogin
		{
			get
			{
				return _requiresLogin;
			}
			set
			{
				_requiresLogin = value;
			}
		}

		/// <summary>
		/// Whether to redirect to the canvas page, *if* a redirect happens because the
		/// user is being forced to add the application or log into it.
		/// </summary>
		[
		Category("Behavior"),
		DefaultValue(false),
		Description("Send to canvas in case of log in or add")
		]
		public bool RedirectCanvas
		{
			get
			{
				return _redirectCanvas;
			}
			set
			{
				_redirectCanvas = value;
			}
		}

		/// <summary>
		/// Append this to callback url when redirecting to login or add. After the user has
		/// added the app or logged into it, facebook will redirect directly to the callback URL,
		/// or to the canvas page if the RedirectCanvas property is true. 
		/// This next parameter is appended to that callback url, and would typically be the name
		/// of a specific page, plus query string parameters if those are needed, like so:
		/// ...RedirectNext = "SomePage.aspx?mydata1=101&amp;mydata2=somedata";
		/// 
		/// If this is not static, it should be set up in the page's OnPreInit(), before 
		/// the call to the base class, so that this preceeds the actual redirect.
		/// </summary>
		[
		Category("Behavior"),
		DefaultValue(""),
		Description("Suffix for callback url when redirecting to login or add")
		]
		public string RedirectNext
		{
			get
			{
				return _redirectNext;
			}
			set
			{
				_redirectNext = value;
			}
		}

		/// <summary>
		/// Gets the configuration settings for this application if the ApplicationName
		/// property is set.
		/// </summary>
		[Browsable(false)]
		public FacebookApplicationSettings ApplicationSettings
		{
			get
			{
				if (String.IsNullOrEmpty(_applicationName) == false)
				{
					return FacebookSection.GetApplication(_applicationName);
				}
				return null;
			}
		}

		/// <summary>
		/// Whether to enable rendering of Ajax support functionality in IFrame
		/// pages.
		/// Specifically it adds the fbContext global variable which identifies the
		/// application and session information, which may be consumed by client
		/// applications to use when invoking the FacebookProxy.axd service. The
		/// path to the proxy itself is written out as the fbProxy global variable.
		/// You must set ApplicationName in order to use this feature.
		/// </summary>
		[
		Category("IFrame Behavior"),
		DefaultValue(false),
		Description("Enable rendering of Ajax support features.")
		]
		public bool EnableAjaxScenarios
		{
			get
			{
				return _enableAjaxScenarios;
			}
			set
			{
				_enableAjaxScenarios = value;
			}
		}

		/// <summary>
		/// Whether to enable debug information to be sent as part of the response in
		/// the case of an error.
		/// This can only be set during initialization of the page.
		/// </summary>
		[
		Category("FBML Behavior"),
		DefaultValue(false),
		Description("Enables debugging features for FBML applications.")
		]
		public bool EnableDebugging
		{
			get
			{
				return _enableDebugging;
			}
			set
			{
				if (_session != null)
				{
					throw new InvalidOperationException("EnableDebugging can only be set during page initialization.");
				}
				_enableDebugging = value;
			}
		}

		/// <summary>
		/// Whether to enable IFrame pages to be browsed externally as a regular Web application, i.e.,
		/// outside Facebook when a particular user's session information is not available.
		/// This can only be set during initialization of the page.
		/// </summary>
		[
		Category("IFrame Behavior"),
		DefaultValue(false),
		Description("Enable regular browsing of this page external to Facebook.")
		]
		public bool EnableExternalBrowsing
		{
			get
			{
				return _enableExternalBrowsing;
			}
			set
			{
				if (_session != null)
				{
					throw new InvalidOperationException("RenderExternalBrowsing can only be set during page initialization.");
				}
				_enableExternalBrowsing = value;
			}
		}

		/// <internalonly />
		[
		Browsable(false),
		DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden),
		EditorBrowsable(EditorBrowsableState.Never)
		]
		public override bool EnableTheming
		{
			get
			{
				return false;
			}
			set
			{
				throw new NotSupportedException();
			}
		}

		/// <internalonly />
		[
		Browsable(false),
		DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden),
		EditorBrowsable(EditorBrowsableState.Never)
		]
		public override bool EnableViewState
		{
			get
			{
				return false;
			}
			set
			{
				throw new NotSupportedException();
			}
		}

		/// <summary>
		/// Whether the application has been added by the current user.
		/// </summary>
		[Browsable(false)]
		public bool IsApplicationAdded
		{
			get
			{
				return _isAppAdded;
			}
		}

		/// <summary>
		/// Whether the request is being made in the context of a canvas page.
		/// </summary>
		[Browsable(false)]
		public bool InCanvasPage
		{
			get
			{
				return _inCanvasPage;
			}
		}

		/// <summary>
		/// Whether the request is being made in the context of an iframe page.
		/// </summary>
		[Browsable(false)]
		public bool InIFramePage
		{
			get
			{
				return _inIFramePage;
			}
		}

		/// <summary>
		/// Whether the request is coming from within the new profile page or not.
		/// </summary>
		[Browsable(false)]
		public bool InNewProfile
		{
			get
			{
				return _inNewProfile;
			}
		}

		/// <summary>
		/// Indicates whether a Facebook session has been established or not.
		/// </summary>
		[Browsable(false)]
		public bool IsSessionCreated
		{
			get
			{
				return (_session != null);
			}
		}

		/// <summary>
		/// The type of user interface used by the Facebook application.
		/// This can only be set during initialization of the page.
		/// </summary>
		[
		Category("Behavior"),
		DefaultValue(FacebookApplicationMode.IFrame),
		Description("The type of Facebook application.")
		]
		public FacebookApplicationMode Mode
		{
			get
			{
				return _mode;
			}
			set
			{
				if ((value < FacebookApplicationMode.IFrame) || (value > FacebookApplicationMode.Fbml))
				{
					throw new ArgumentOutOfRangeException("value");
				}
				if (_session != null)
				{
					throw new InvalidOperationException("Mode can only be set during page initialization.");
				}
				_mode = value;
			}
		}

		/// <summary>
		/// The ID of the user browsing the page that the application is associated with.
		/// </summary>
		[Browsable(false)]
		public string PageUserID
		{
			get
			{
				return Session.PageUserID;
			}
		}

		/// <summary>
		/// The application secret used to authenticate API calls issued by the application that is
		/// available at application registration time.
		/// This can only be set during initialization of the page.
		/// </summary>
		[
		Category("Behavior"),
		DefaultValue(""),
		Description("The application secret that is used to authenticate API calls.")
		]
		public string Secret
		{
			get
			{
				if (_secret == null)
				{
					return String.Empty;
				}
				return _secret;
			}
			set
			{
				if (_session != null)
				{
					throw new InvalidOperationException("Secret can only be set during page initialization.");
				}
				_secret = value;
			}
		}

		/// <summary>
		/// The Facebook service providing access to the Facebook APIs.
		/// This is only available once a Facebook session has been established.
		/// </summary>
		[Browsable(false)]
		public FacebookService Service
		{
			get
			{
				if (_service == null)
				{
					throw new InvalidOperationException("Service can only be accessed after a Facebook session is established.");
				}
				return _service;
			}
		}

		/// <summary>
		/// The current Facebook session if one has been established. A session
		/// is established after page initialization is complete. The SessionCreated
		/// event signals successful creation of a session.
		/// </summary>
		[Browsable(false)]
		public FacebookSession Session
		{
			get
			{
				if (_session == null)
				{
					throw new InvalidOperationException("Session can only be accessed after a Facebook session is established.");
				}
				return _session;
			}
		}

		/// <summary>
		/// The type of the active session.
		/// </summary>
		[Browsable(false)]
		public FacebookSessionType SessionType
		{
			get
			{
				return Session.SessionType;
			}
		}

		/// <summary>
		/// The unique ID of the user associated with the current Facebook session.
		/// If a Facebook session has not been established, it returns an empty string.
		/// If the session is associated with a Facebook Page, this returns the
		/// ID of the Page instead. In that scenario, the ID of the user itself is
		/// accessible via the PageUserID property.
		/// </summary>
		[Browsable(false)]
		public string UserID
		{
			get
			{
				if (_service == null)
				{
					return String.Empty;
				}
				return _service.UserID;
			}
		}

		/// <summary>
		/// Whether the control is visible. A FacebookApplication control should always be
		/// set to visible.
		/// </summary>
		[
		Browsable(false),
		DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden),
		EditorBrowsable(EditorBrowsableState.Never)
		]
		public override bool Visible
		{
			get
			{
				return base.Visible;
			}
			set
			{
				if (value == false)
				{
					throw new NotSupportedException();
				}
				base.Visible = value;
			}
		}

		/// <summary>
		/// Raised when a Facebook session has been created.
		/// </summary>
		[
		Category("Behavior"),
		Description("Raised whan a Facebook session has been created, and the Facebook service can be used.")
		]
		public event EventHandler SessionCreated
		{
			add
			{
				Events.AddHandler(SessionCreatedEvent, value);
			}
			remove
			{
				Events.RemoveHandler(SessionCreatedEvent, value);
			}
		}

		/// <summary>
		/// Raised when a Facebook session is being created.
		/// This event is only raised in IFrame apps. It is useful for
		/// directing FacebookApplication to create a session using an infinite
		/// key when the page is being browsed externally, i.e. outside
		/// of Facebook, by virtue of setting EnableExternalBrowsing to true.
		/// </summary>
		[
		Category("Behavior"),
		Description("Raised whan a Facebook session is being created.")
		]
		public event FacebookSessionCreatingEventHandler SessionCreating
		{
			add
			{
				Events.AddHandler(SessionCreatingEvent, value);
			}
			remove
			{
				Events.RemoveHandler(SessionCreatingEvent, value);
			}
		}

		/// <summary>
		/// Gets the current FacebookApplication control instance on the page.
		/// </summary>
		/// <param name="page">The page that contains the control; should be the current page instance.</param>
		/// <returns>The FacebookApplication instance if there is one; null otherwise.</returns>
		public static FacebookApplication GetCurrent(Page page)
		{
			return page.Items[typeof(FacebookApplication)] as FacebookApplication;
		}

		/// <summary>
		/// Loads the state of the control.
		/// </summary>
		/// <param name="savedState">The previously saved state.</param>
		protected override void LoadControlState(object savedState)
		{
			_controlState = savedState;
		}

		/// <summary>
		/// Initializes the control, and registers it with the page as the current
		/// FacebookApplication instance.
		/// </summary>
		/// <param name="e"></param>
		protected override void OnInit(EventArgs e)
		{
			base.OnInit(e);

			if (DesignMode)
			{
				return;
			}

			FacebookApplication registeredApp = GetCurrent(Page);
			if (registeredApp != null)
			{
				throw new InvalidOperationException("You can only have one instance of a FacebookApplication control on the page.");
			}

			Page.Items[typeof(FacebookApplication)] = this;
			Page.PreLoad += OnPagePreLoad;
			Page.Error += OnPageError;

			Page.RegisterRequiresControlState(this);

			HttpRequest request = Context.Request;
			_isAppAdded = (String.CompareOrdinal(request["fb_sig_added"], "1") == 0) ||
						  (String.IsNullOrEmpty(request["fb_page_id"]) == false);
			_inCanvasPage = (String.CompareOrdinal(request["fb_sig_in_canvas"], "1") == 0);
			_inIFramePage = (String.CompareOrdinal(request["fb_sig_in_iframe"], "1") == 0);
			_inNewProfile = (String.CompareOrdinal(request["fb_sig_in_new_facebook"], "1") == 0);	
		}

		private void OnPageError(object sender, EventArgs e)
		{
			if (_mode != FacebookApplicationMode.Fbml)
			{
				return;
			}

			HttpRequest request = Page.Request;
			HttpResponse response = Page.Response;

			response.Write("<div style=\"padding: 10px\">");
			if (_enableDebugging == false)
			{
				response.Write("<p>Sorry, there was an error processing the request. Please refresh the page to try again.</p>");
			}
			else
			{
				response.Write("<h1>Exception Information</h1>");
				Exception ex = Context.Error;
				if (ex != null)
				{
					response.Write("<p>");
					response.Write(ex.Message);
					response.Write("</p>");
					response.Write("<pre>");
					response.Write(ex.StackTrace);
					response.Write("</pre>");
				}
				else
				{
					response.Write("<p>Unknown error</p>");
				}

				response.Write("<hr />");

				response.Write("<h1>Request Form Data</h1>");
				response.Write("<pre>");
				foreach (string s in request.Form)
				{
					response.Write(s);
					response.Write(" = ");
					response.Write(request.Form[s]);
					response.Write("<br />");
				}
				response.Write("</pre>");
			}

			response.Write("</div>");

			Context.ClearError();
			response.End();
		}

		private void OnPagePreLoad(object sender, EventArgs e)
		{
			if (String.IsNullOrEmpty(_applicationKey) || String.IsNullOrEmpty(_secret))
			{
				string appName = ApplicationName;

				if (appName.Length != 0)
				{
					FacebookApplicationSettings appSettings = ApplicationSettings;

					if (appSettings != null)
					{
						_applicationKey = appSettings.ApiKey;
						_secret = appSettings.Secret;

						Debug.Assert(String.IsNullOrEmpty(_applicationKey) == false);
						Debug.Assert(String.IsNullOrEmpty(_secret) == false);
					}
					else
					{
						throw new InvalidOperationException("The specified application name is not a valid application registered in configuration.");
					}
				}
			}

			if (String.IsNullOrEmpty(_applicationKey))
			{
				throw new InvalidOperationException("The ApplicationKey must be set during page initialization.");
			}
			if (String.IsNullOrEmpty(_secret))
			{
				throw new InvalidOperationException("The Secret must be set during page initialization.");
			}

			FacebookSessionCreatingEventArgs creatingEventArgs = new FacebookSessionCreatingEventArgs();
			if ((_mode == FacebookApplicationMode.IFrame) && (InIFramePage == false))
			{
				OnSessionCreating(creatingEventArgs);
			}

			if (creatingEventArgs.SessionKey != null)
			{
				// Create a service using the developer supplied session information.
				// Creating the service with this information creates an infinite session object
				// under the covers, which we'll pull out and hang onto, rather than
				// create an actual server session object.
				// This is useful in the external browsing scenario.

				_service = new FacebookService(_applicationKey, _secret,
											   creatingEventArgs.SessionKey,
											   creatingEventArgs.UserID, creatingEventArgs.PageUserID);
				_session = _service.Session;

				OnSessionCreated(EventArgs.Empty);
			}
			else
			{
				FacebookServerSession session;
				if (_mode == FacebookApplicationMode.IFrame)
				{
					session = new FacebookIFrameSession(this, _applicationKey, _secret);
					if (_controlState != null)
					{
						session.LoadState(_controlState);
						_controlState = null;
					}
				}
				else
				{
					session = new FacebookFbmlSession(this, _applicationKey, _secret);
				}

				string redirectUrl = session.Initialize(Context);
				if (redirectUrl == null)
				{
					_session = session;
					_service = new FacebookService(_session);
					OnSessionCreated(EventArgs.Empty);
				}
				else
				{
					if ((_mode != FacebookApplicationMode.IFrame) || InIFramePage ||
						(_enableExternalBrowsing == false))
					{
						Redirect(redirectUrl, /* detectRelativeUrl */ false);
					}
				}
			}
		}

		/// <summary>
		/// Prepares the control for rendering.
		/// </summary>
		/// <param name="e">The event information associated with the event.</param>
		protected override void OnPreRender(EventArgs e)
		{
			base.OnPreRender(e);

			if ((_mode == FacebookApplicationMode.IFrame) && InIFramePage)
			{
				Page.Response.AppendHeader("P3P", "CP=\"CAO PSA OUR\"");
			}
		}

		/// <summary>
		/// Raises the SessionCreated event upon successful creation of a Facebook session.
		/// </summary>
		/// <param name="e">The event information associated with the event.</param>
		protected virtual void OnSessionCreated(EventArgs e)
		{
			EventHandler handler = (EventHandler)Events[SessionCreatedEvent];
			if (handler != null)
			{
				handler(this, e);
			}
		}

		/// <summary>
		/// Raises the SessionCreating event before creation of a Facebook session.
		/// </summary>
		/// <param name="e">The event information associated with the event.</param>
		protected virtual void OnSessionCreating(FacebookSessionCreatingEventArgs e)
		{
			FacebookSessionCreatingEventHandler handler =
				(FacebookSessionCreatingEventHandler)Events[SessionCreatingEvent];
			if (handler != null)
			{
				handler(this, e);
			}
		}

		/// <summary>
		/// Performs a redirect appropriately based on the type of Facebook application.
		/// </summary>
		/// <param name="contextControl">The page or user control from within which the call is being made.</param>
		/// <param name="url">The URL to redirect to.</param>
		public void Redirect(Control contextControl, string url)
		{
			if (String.IsNullOrEmpty(url))
			{
				throw new ArgumentNullException("url");
			}

			url = contextControl.ResolveClientUrl(url);
			Redirect(url, /* detectRelativeUrl */ true);
		}

		private void Redirect(string url, bool detectRelativeUrl)
		{
			Debug.Assert(String.IsNullOrEmpty(url) == false);

			if ((_session != null) &&
				(_session.SessionType == FacebookSessionType.PresencePage))
			{
				int queryIndex = url.IndexOf('?');
				url += (queryIndex >= 0) ? "&" : "?";
				url += "fb_page_id=" + UserID;
			}

			HttpResponse response = Page.Response;
			if (InCanvasPage)
			{
				response.Write("<fb:redirect url=\"" + url + "\" />");
			}
			else if (InIFramePage)
			{
				if (detectRelativeUrl)
				{
					Debug.Assert(_session is FacebookIFrameSession);
					url = ((FacebookIFrameSession)_session).ResolveUrl(Context, url);
				}

				response.Write(String.Format(@"<script type=""text/javascript"">window.top.location.href='{0}';</script>", url));
			}
			else
			{
				// A regular HTTP redirect. This really only makes sense if the
				// application is an IFrame application, and its being browsed
				// externally.
				response.Redirect(url);
			}
			response.End();
		}

		/// <summary>
		/// Renders the control into markup.
		/// </summary>
		/// <param name="writer">The writer into which the markup is rendered.</param>
		protected override void Render(HtmlTextWriter writer)
		{
			if ((_mode == FacebookApplicationMode.IFrame) && InIFramePage)
			{
				if (_enableAjaxScenarios && (ApplicationName.Length == 0))
				{
					throw new InvalidOperationException("You must set ApplicationName in order to set EnableAjaxScenarios.");
				}

				string script = String.Format(@"<script type=""text/javascript"">window.fbContext = '{0};{1};{2};{3}'; window.fbUserID = '{2}'; window.fbPageUserID = '{3}'; window.fbProxy = '{4}';</script>",
											  ApplicationName, _session.SessionKey, _session.UserID, _session.PageUserID,
											  ResolveClientUrl("~/FacebookProxy.axd"));
				writer.Write(script);
			}
		}

		/// <summary>
		/// Saves control state associated with the control.
		/// </summary>
		/// <returns>The state of the control to be preserved.</returns>
		protected override object SaveControlState()
		{
			if ((_session != null) && (_session is FacebookIFrameSession))
			{
				return ((FacebookIFrameSession)_session).SaveState();
			}
			return null;
		}
	}
}
